# This file is part of Indico.
# Copyright (C) 2002 - 2025 CERN
#
# Indico is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see the
# LICENSE file for more details.

from functools import partial
from itertools import chain

from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.event import listen
from sqlalchemy.ext.hybrid import Comparator, hybrid_property

from indico.core import signals
from indico.core.db import db
from indico.core.db.sqlalchemy import PyIntEnum
from indico.core.db.sqlalchemy.custom.utcdatetime import UTCDateTime
from indico.core.logger import Logger
from indico.modules.events.models.events import Event
from indico.modules.users.models.users import User
from indico.modules.vc.notifications import notify_deleted
from indico.util.caching import memoize_request
from indico.util.date_time import now_utc
from indico.util.enum import IndicoIntEnum
from indico.util.string import format_repr


class VCRoomLinkType(IndicoIntEnum):
    event = 1
    contribution = 2
    block = 3


_columns_for_types = {
    VCRoomLinkType.event: {'linked_event_id'},
    VCRoomLinkType.contribution: {'contribution_id'},
    VCRoomLinkType.block: {'session_block_id'},
}


def _make_checks():
    available_columns = set(chain.from_iterable(cols for type_, cols in _columns_for_types.items()))
    for link_type in VCRoomLinkType:
        required_cols = available_columns & _columns_for_types[link_type]
        forbidden_cols = available_columns - required_cols
        criteria = [f'{col} IS NULL' for col in sorted(forbidden_cols)]
        criteria += [f'{col} IS NOT NULL' for col in sorted(required_cols)]
        condition = 'link_type != {} OR ({})'.format(link_type, ' AND '.join(criteria))
        yield db.CheckConstraint(condition, f'valid_{link_type.name}_link')


class VCRoomStatus(IndicoIntEnum):
    created = 1
    deleted = 2


class VCRoom(db.Model):
    __tablename__ = 'vc_rooms'
    __table_args__ = (db.Index(None, 'data', postgresql_using='gin'),
                      {'schema': 'events'})

    #: Videoconference room ID
    id = db.Column(
        db.Integer,
        primary_key=True
    )
    #: Type of the videoconference room
    type = db.Column(
        db.String,
        nullable=False
    )
    #: Name of the videoconference room
    name = db.Column(
        db.String,
        nullable=False
    )
    #: Status of the videoconference room
    status = db.Column(
        PyIntEnum(VCRoomStatus),
        nullable=False
    )
    #: ID of the creator
    created_by_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        nullable=False,
        index=True
    )
    #: Creation timestamp of the videoconference room
    created_dt = db.Column(
        UTCDateTime,
        nullable=False,
        default=now_utc
    )

    #: Modification timestamp of the videoconference room
    modified_dt = db.Column(
        UTCDateTime
    )
    #: videoconference plugin-specific data
    data = db.Column(
        JSONB,
        nullable=False
    )

    #: The user who created the videoconference room
    created_by_user = db.relationship(
        'User',
        lazy=True,
        backref=db.backref(
            'vc_rooms',
            lazy='dynamic'
        )
    )

    # relationship backrefs:
    # - events (VCRoomEventAssociation.vc_room)

    @property
    def plugin(self):
        from indico.modules.vc.util import get_vc_plugins
        return get_vc_plugins().get(self.type)

    @property
    def locator(self):
        return {'vc_room_id': self.id, 'service': self.type}

    def __repr__(self):
        return f'<VCRoom({self.id}, {self.name}, {self.type})>'

    def delete(self, user: User, event: Event | None = None):
        """Delete a VC room and all its associations.

        :param user: the user performing the deletion
        :param event: the event in which the deletion happened
        """
        for assoc in self.events[:]:
            Logger.get('modules.vc').info('Detaching videoconference %s from event %s (%s)',
                                          self, assoc.event, assoc.link_object)
            assoc.delete(user, check_vc_room=False)
        db.session.flush()

        # send signal
        signals.vc.vc_room_deleted.send(self, event=event)

        # process plugin actions
        Logger.get('modules.vc').info(f'Deleting videoconference {self}')
        if self.status != VCRoomStatus.deleted and self.plugin:
            self.plugin.delete_room(self, event)
            notify_deleted(self.plugin, self, self, event, user)
        db.session.delete(self)


class VCRoomEventAssociation(db.Model):
    __tablename__ = 'vc_room_events'
    __table_args__ = (*_make_checks(), db.Index(None, 'data', postgresql_using='gin'), {'schema': 'events'})

    #: Association ID
    id = db.Column(
        db.Integer,
        primary_key=True
    )

    #: ID of the event
    event_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        index=True,
        autoincrement=False,
        nullable=False
    )
    #: ID of the videoconference room
    vc_room_id = db.Column(
        db.Integer,
        db.ForeignKey('events.vc_rooms.id'),
        index=True,
        nullable=False
    )
    #: Type of the object the vc_room is linked to
    link_type = db.Column(
        PyIntEnum(VCRoomLinkType),
        nullable=False
    )
    linked_event_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        index=True,
        nullable=True
    )
    session_block_id = db.Column(
        db.Integer,
        db.ForeignKey('events.session_blocks.id'),
        index=True,
        nullable=True
    )
    contribution_id = db.Column(
        db.Integer,
        db.ForeignKey('events.contributions.id'),
        index=True,
        nullable=True
    )
    #: If the vc room should be shown on the event page
    show = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: videoconference plugin-specific data
    data = db.Column(
        JSONB,
        nullable=False
    )

    #: The associated :class:VCRoom
    vc_room = db.relationship(
        'VCRoom',
        lazy=False,
        backref=db.backref('events', cascade='all, delete-orphan')
    )
    #: The associated Event
    event = db.relationship(
        'Event',
        foreign_keys=event_id,
        lazy=True,
        backref=db.backref(
            'all_vc_room_associations',
            lazy='dynamic'
        )
    )
    #: The linked event (if the VC room is attached to the event itself)
    linked_event = db.relationship(
        'Event',
        foreign_keys=linked_event_id,
        lazy=True,
        backref=db.backref(
            'vc_room_associations',
            lazy=True
        )
    )
    #: The linked contribution (if the VC room is attached to a contribution)
    linked_contrib = db.relationship(
        'Contribution',
        lazy=True,
        backref=db.backref(
            'vc_room_associations',
            lazy=True
        )
    )
    #: The linked session block (if the VC room is attached to a block)
    linked_block = db.relationship(
        'SessionBlock',
        lazy=True,
        backref=db.backref(
            'vc_room_associations',
            lazy=True
        )
    )

    @classmethod
    def register_link_events(cls):
        event_mapping = {cls.linked_block: lambda x: x.event,
                         cls.linked_contrib: lambda x: x.event,
                         cls.linked_event: lambda x: x}

        type_mapping = {cls.linked_event: VCRoomLinkType.event,
                        cls.linked_block: VCRoomLinkType.block,
                        cls.linked_contrib: VCRoomLinkType.contribution}

        def _set_link_type(link_type, target, value, *unused):
            if value is not None:
                target.link_type = link_type

        def _set_event_obj(fn, target, value, *unused):
            if value is not None:
                event = fn(value)
                assert event is not None
                target.event = event

        for rel, fn in event_mapping.items():
            if rel is not None:
                listen(rel, 'set', partial(_set_event_obj, fn))

        for rel, link_type in type_mapping.items():
            if rel is not None:
                listen(rel, 'set', partial(_set_link_type, link_type))

    @property
    def locator(self):
        return dict(self.event.locator, service=self.vc_room.type, event_vc_room_id=self.id)

    @hybrid_property
    def link_object(self):
        if self.link_type == VCRoomLinkType.event:
            return self.linked_event
        elif self.link_type == VCRoomLinkType.contribution:
            return self.linked_contrib
        else:
            return self.linked_block

    @link_object.setter
    def link_object(self, obj):
        self.linked_event = self.linked_contrib = self.linked_block = None
        if isinstance(obj, db.m.Event):
            self.linked_event = obj
        elif isinstance(obj, db.m.Contribution):
            self.linked_contrib = obj
        elif isinstance(obj, db.m.SessionBlock):
            self.linked_block = obj
        else:
            raise TypeError(f'Unexpected object: {obj}')

    @link_object.comparator
    def link_object(cls):
        return _LinkObjectComparator(cls)

    def __repr__(self):
        return format_repr(self, 'id', 'link_object', 'vc_room')

    @classmethod
    def find_for_event(cls, event, include_hidden=False, include_deleted=False, only_linked_to_event=False, **kwargs):
        """Return a Query that retrieves the videoconference rooms for an event.

        :param event: an indico Event
        :param only_linked_to_event: only retrieve the vc rooms linked to the whole event
        :param kwargs: extra kwargs to pass to ``filter_by()``
        """
        if only_linked_to_event:
            kwargs['link_type'] = int(VCRoomLinkType.event)
        query = event.all_vc_room_associations
        if kwargs:
            query = query.filter_by(**kwargs)
        if not include_hidden:
            query = query.filter(cls.show)
        if not include_deleted:
            query = query.filter(VCRoom.status != VCRoomStatus.deleted).join(VCRoom)
        return query

    @classmethod
    @memoize_request
    def get_linked_for_event(cls, event):
        """Get a dict mapping link objects to event vc rooms."""
        return {vcr.link_object: vcr for vcr in cls.find_for_event(event)}

    def delete(self, user: User, *, check_vc_room: bool = True):
        """Delete a VC room from an event.

        If the room is not used anywhere else, the room itself is also deleted.

        :param user: the user performing the deletion
        :param check_vc_room: whether to check as well if the VCRoom can be deleted
                              (no more associations left)
        """
        # send signals
        signals.vc.vc_room_detached.send(self, vc_room=self.vc_room, old_link=self.link_object, event=self.event)

        Logger.get('modules.vc').info(
            'Detaching videoconference %s from event %s (%s)', self.vc_room, self.event, self.link_object
        )

        # do the actual deletion from the DB
        vc_room = self.vc_room
        self.vc_room.events.remove(self)
        db.session.flush()

        # process plugin actions
        if plugin := vc_room.plugin:
            plugin.detach_room(self, vc_room, self.event)

        # if there are no associations left,
        if check_vc_room and not vc_room.events:
            # delete also the VCRoom itself
            vc_room.delete(user, event=self.event)


VCRoomEventAssociation.register_link_events()


class _LinkObjectComparator(Comparator):
    def __init__(self, cls):
        self.cls = cls

    def __clause_element__(self):
        # just in case
        raise NotImplementedError

    def __eq__(self, other):
        if isinstance(other, db.m.Event):
            return db.and_(self.cls.link_type == VCRoomLinkType.event,
                           self.cls.linked_event_id == other.id)
        elif isinstance(other, db.m.SessionBlock):
            return db.and_(self.cls.link_type == VCRoomLinkType.block,
                           self.cls.session_block_id == other.id)
        elif isinstance(other, db.m.Contribution):
            return db.and_(self.cls.link_type == VCRoomLinkType.contribution,
                           self.cls.contribution_id == other.id)
        else:
            raise TypeError(f'Unexpected object type {type(other)}: {other}')
